"""
@author: 江同学呀
@file: compound_queries.py
@date: 2025/1/11 14:52
@desc:
    复合查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/compound-queries.html

    Compound queries wrap other compound or leaf queries, either to combine their results and scores, to change their
    behaviour, or to switch from query to filter context.
    复合查询包装其他复合或叶查询，以合并其结果和分数、更改其行为或从查询切换到筛选器上下文。
"""
from typing import Union, List, Dict, Iterable, Literal

from espc.common.common import Number
from espc.common.query_common import ScoreMode, BoostMode
from espc.err import ESPCInvalidValueRangeError
from espc.orm.model.dsl.queries.base_queries import _BaseQueries
from espc.orm.model.struct.field_value_factor import FieldValueFactor
from espc.orm.model.struct.functions import DecayFunctions
from espc.orm.model.struct.random_score import RandomScore
from espc.orm.model.scripting.script import Script


class Boolean(_BaseQueries):
    """
    布尔查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-bool-query.html

    A query that matches documents matching boolean combinations of other queries. The bool query maps to Lucene
    BooleanQuery. It is built using one or more boolean clauses, each clause with a typed occurrence. The occurrence
    types are:
        must        The clause (query) must appear in matching documents and will contribute to the score.
        filter      The clause (query) must appear in matching documents. However unlike must the score of the query
                    will be ignored. Filter clauses are executed in filter context, meaning that scoring is ignored and
                    clauses are considered for caching.
        should      The clause (query) should appear in the matching document.
        must_not    The clause (query) must not appear in the matching documents. Clauses are executed in filter context
                    meaning that scoring is ignored and clauses are considered for caching. Because scoring is ignored,
                    a score of 0 for all documents is returned.

    一个查询，它与匹配其他查询的布尔组合的文档匹配。bool 查询映射到 Lucene BooleanQuery。它是使用一个或多个布尔子句构建的，每个子句都有
    一个类型化的匹配项。匹配项类型包括：
        must        子句 （query） 必须出现在匹配的文档中，并且将对分数产生影响。
        filter      子句 （query） 必须出现在匹配的文档中。但是，与 must 不同的是，查询的分数将被忽略。Filter 子句在过滤器上下文中执行，这意味着将忽略评分，并考虑对子句进行缓存。
        should      子句 （query） 应出现在匹配的文档中。
        must_not    子句 （query） 不得出现在匹配的文档中。子句在筛选器上下文中执行，这意味着将忽略评分，并考虑对子句进行缓存。由于忽略了评分，因此将返回所有文档的分数 0。

    :param must:
    :param filter_:
    :param should:
    :param must_not:
    """
    type: str = "bool"

    def __init__(
            self, must: Union[_BaseQueries, List[_BaseQueries]] = None,
            filter_: Union[_BaseQueries, List[_BaseQueries]] = None,
            should: Union[_BaseQueries, List[_BaseQueries]] = None,
            must_not: Union[_BaseQueries, List[_BaseQueries]] = None
    ):
        super().__init__()
        self._must: Union[_BaseQueries, List[_BaseQueries]] = must
        self._filter: Union[_BaseQueries, List[_BaseQueries]] = filter_
        self._should: Union[_BaseQueries, List[_BaseQueries]] = should
        self._must_not: Union[_BaseQueries, List[_BaseQueries]] = must_not
        return

    def _build(self) -> Dict:
        _body: Dict = {}
        if self._must:
            if isinstance(self._must, _BaseQueries):
                _body["must"] = {self._must.type: self._must._build()}
            else:
                _body["must"] = [{_must.type: _must._build()} for _must in self._must]
        if self._filter:
            if isinstance(self._filter, _BaseQueries):
                _body["filter"] = {self._filter.type: self._filter._build()}
            else:
                _body["filter"] = [{_filter.type: _filter._build()} for _filter in self._filter]
        if self._should:
            if isinstance(self._should, _BaseQueries):
                _body["should"] = {self._should.type: self._should._build()}
            else:
                _body["should"] = [{_should.type: _should._build()} for _should in self._should]
        if self._must_not:
            if isinstance(self._must_not, _BaseQueries):
                _body["must_not"] = {self._must_not.type: self._must_not._build()}
            else:
                _body["must_not"] = [{_must_not.type: _must_not._build()} for _must_not in self._must_not]
        return _body


class Boosting(_BaseQueries):
    """
    提升查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-boosting-query.html

    Returns documents matching a positive query while reducing the relevance score of documents that also match a negative query.
    You can use the boosting query to demote certain documents without excluding them from the search results.

    返回与肯定查询匹配的文档，同时降低与否定查询匹配的文档的相关性分数。
    您可以使用 boosting 查询降级某些文档，而不会将其从搜索结果中排除。

    :param positive:
        (Required, query object) Query you wish to run. Any returned documents must match this query.
        （必需，查询对象）Query 中。任何返回的文档都必须与此查询匹配。
    :param negative:
        (Required, query object) Query used to decrease the relevance score of matching documents.
        If a returned document matches the positive query and this query, the boosting query calculates the final
        relevance score for the document as follows:
            - Take the original relevance score from the positive query.
            - Multiply the score by the negative_boost value.

        （必需，查询对象）Query 用于降低匹配文档的相关性分数。
        如果返回的文档与肯定查询和此查询匹配，则提升查询将计算文档的最终相关性分数，如下所示：
            - 从肯定查询中获取原始相关性分数。
            - 将分数乘以 negative_boost 值。
    :param negative_boost:
        (Required, float) Floating point number between 0 and 1.0 used to decrease the relevance scores of documents
        matching the negative query.
        （必需，浮点数）介于 0 和 1.0 之间的浮点数，用于降低与否定查询匹配的文档的相关性分数。
    """
    type: str = "boosting"

    def __init__(self, positive: _BaseQueries, negative: _BaseQueries, negative_boost: float):
        if negative_boost > 1.0 or negative_boost < 0.0:
            raise ESPCInvalidValueRangeError("negative_boost必须在0和1之间")
        super().__init__()
        self._positive: _BaseQueries = positive
        self._negative: _BaseQueries = negative
        self._negative_boost: float = negative_boost
        return

    def _build(self) -> Dict:
        return {
            "positive": {
                self._positive.type: self._positive._build()
            },
            "negative": {
                self._negative.type: self._negative._build()
            },
            "negative_boost": self._negative_boost
        }


class ConstantScore(_BaseQueries):
    """
    常量分数查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-constant-score-query.html

    Wraps a filter query and returns every matching document with a relevance score equal to the boost parameter value.
    包装筛选条件查询并返回相关性分数等于 boost 参数值的每个匹配文档。

    :param filter_:
        (Required, query object) Filter query you wish to run. Any returned documents must match this query.
        Filter queries do not calculate relevance scores. To speed up performance, Elasticsearch automatically caches
        frequently used filter queries.

        （必需，查询对象）筛选要运行的查询。任何返回的文档都必须与此查询匹配。
        筛选条件查询不计算相关性分数。为了提高性能，Elasticsearch 会自动缓存常用的筛选条件查询。
    :param boost:
        (Optional, float) Floating point number used as the constant relevance score for every document matching the
        filter query. Defaults to 1.0.
        （可选，浮点数）浮点数，用作与过滤器查询匹配的每个文档的常量相关性分数。默认为 1.0。
    """
    type: str = "constant_score"

    def __init__(self, filter_: _BaseQueries, boost: float = None):
        super().__init__()
        self._filter: _BaseQueries = filter_
        self._boost: float = boost
        return

    def _build(self) -> Dict:
        _body: Dict = {
            "filter": {
                self._filter.type: self._filter._build()
            }
        }
        if self._boost is not None:
            _body["boost"] = self._boost
        return _body


class DisjunctionMax(_BaseQueries):
    """
    最大或查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-dis-max-query.html

    Returns documents matching one or more wrapped queries, called query clauses or clauses.
    If a returned document matches multiple query clauses, the dis_max query assigns the document the highest relevance
    score from any matching clause, plus a tie breaking increment for any additional matching subqueries.
    You can use the dis_max to search for a term in fields mapped with different boost factors.

    返回与一个或多个包装查询（称为查询子句或子句）匹配的文档。
    如果返回的文档与多个查询子句匹配，则 dis_max 查询会为文档分配任何匹配子句的最高相关性分数，以及任何其他匹配子查询的平局中断增量。
    您可以使用dis_max在映射了不同提升因子的字段中搜索词。

    :param queries:
        (Required, array of query objects) Contains one or more query clauses. Returned documents must match one or
        more of these queries. If a document matches multiple queries, Elasticsearch uses the highest relevance score.
        （必需，查询对象数组）包含一个或多个查询子句。返回的文档必须与这些查询中的一个或多个匹配。如果文档与多个查询匹配，则 Elasticsearch 使用最高的相关性分数。
    :param tie_breaker:
        (Optional, float) Floating point number between 0 and 1.0 used to increase the relevance scores of documents
        matching multiple query clauses. Defaults to 0.0.
        You can use the tie_breaker value to assign higher relevance scores to documents that contain the same term in
        multiple fields than documents that contain this term in only the best of those multiple fields, without
        confusing this with the better case of two different terms in the multiple fields.
        If a document matches multiple clauses, the dis_max query calculates the relevance score for the document as
        follows:
            Take the relevance score from a matching clause with the highest score.
            Multiply the score from any other matching clauses by the tie_breaker value.
            Add the highest score to the multiplied scores.
        If the tie_breaker value is greater than 0.0, all matching clauses count, but the clause with the highest score
        counts most.

        （可选，浮点数）介于 0 和 1.0 之间的浮点数，用于增加与多个查询子句匹配的文档的相关性分数。默认值为 0.0。
        您可以使用 tie_breaker 值为在多个字段中包含相同术语的文档分配比仅在这些多个字段中最好的包含此术语的文档更高的相关性分数，
        而不会将其与多个字段中两个不同术语的更好情况混淆。
        如果文档与多个子句匹配，则 dis_max 查询将按如下方式计算文档的相关性分数：
            从得分最高的匹配子句中获取相关性分数。
            将任何其他匹配子句的分数乘以 tie_breaker 值。
            将最高分添加到相乘的分数中。
        如果 tie_breaker 值大于 0.0，则所有匹配的子句都计算在内，但得分最高的子句最重要。
    """
    type: str = "dis_max"

    def __init__(self, queries: Iterable[_BaseQueries], tie_breaker: float = None):
        super().__init__()
        self._queries: Iterable[_BaseQueries] = queries
        self._tie_breaker: float = tie_breaker
        return

    def _build(self) -> Dict:
        _body: Dict = {
            "queries": [
                {
                    _query.type: _query._build()
                }
                for _query in self._queries
            ]
        }
        if self._tie_breaker is not None:
            _body["tie_breaker"] = self._tie_breaker
        return _body


class FunctionScore(_BaseQueries):
    """
    函数分数查询
    https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-function-score-query.html

    The function_score allows you to modify the score of documents that are retrieved by a query. This can be useful if,
    for example, a score function is computationally expensive and it is sufficient to compute the score on a filtered
    set of documents.
    To use function_score, the user has to define a query and one or more functions, that compute a new score for each
    document returned by the query.
    function_score can be used with only one function.

    该function_score允许您修改查询检索的文档的分数。例如，如果 score 函数的计算成本很高，并且足以计算一组筛选文档的分数，则这可能很有用。
    要使用function_score，用户必须定义一个查询和一个或多个函数，这些函数为查询返回的每个文档计算新分数。
    function_score 只能与一个函数一起使用。
    
    :param query:
    :param boost:
    :param max_boost:
    :param min_score:
    :param random_score:
    :param boost_mode:
    :param script_score:
    :param weight:
    :param decay_functions:
    :param field_value_factor:
    """
    type: str = "function_score"

    def __init__(
            self, query: _BaseQueries, boost: Number = None, max_boost: Number = None, min_score: Number = None,
            random_score: Union[Dict, RandomScore] = None, script_score: Union[Dict, Script] = None,
            score_mode: Union[Literal["multiply", "sum", "avg", "first", "max", "min"], ScoreMode] = None,
            boost_mode: Union[Literal["multiply", "replace", "sum", "avg", "max", "min"], BoostMode] = None,
            weight: Number = None, decay_functions: Iterable[DecayFunctions] = None,
            field_value_factor: Union[Dict, FieldValueFactor] = None
    ):
        super().__init__()
        self._query: _BaseQueries = query
        self._boost: Number = boost
        self._max_boost: Number = max_boost
        self._min_score: Number = min_score
        self._random_score: Union[Dict, Script] = random_score
        self._score_mode: Union[Literal["multiply", "sum", "avg", "first", "max", "min"], ScoreMode, None] = score_mode
        self._boost_mode: Union[
            Literal[
                "multiply", "replace", "sum", "avg", "max", "min"
            ],
            BoostMode,
            None
        ] = boost_mode
        self._script_score: Union[Dict, Script] = script_score
        self._weight: Number = weight
        self._decay_functions: Iterable[DecayFunctions] = decay_functions
        self._field_value_factor: Union[Dict, FieldValueFactor] = field_value_factor
        return

    def _build(self) -> Dict:
        _body: Dict = {
            "query": {
                self._query.type: self._query._build()
            }
        }
        if self._boost is not None:
            _body["boost"] = self._boost
        if self._max_boost is not None:
            _body["max_boost"] = self._max_boost
        if self._min_score is not None:
            _body["min_score"] = self._min_score
        if self._random_score:
            _body["random_score"] = (
                self._random_score if isinstance(self._random_score, Dict) else self._random_score._build()
            )
        if self._score_mode:
            _body["score_mode"] = self._score_mode if isinstance(self._score_mode, str) else self._score_mode.value
        if self._boost_mode:
            _body["boost_mode"] = self._boost_mode if isinstance(self._boost_mode, str) else self._boost_mode.value
        if self._script_score:
            _body["script_score"] = (
                self._script_score if isinstance(self._script_score, Dict) else self._script_score._build()
            )
        if self._weight is not None:
            _body["weight"] = self._weight
        if self._decay_functions:
            for _decay_function in self._decay_functions:
                _body[_decay_function._func_name] = _decay_function._build()
        if self._field_value_factor:
            _body["field_value_factor"] = (
                self._field_value_factor
                if isinstance(self._field_value_factor, Dict)
                else self._field_value_factor._build()
            )
        return _body


























